﻿/**
 * CsvParser.cpp
 *
 * Copyright (c) 2011, Anders Karlsson
 * All rights reserved.
 * 
 * Redistribution and use in source and binary forms, with or without modification, are permitted provided that the 
 * following conditions are met:
 * 
 * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
 * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following 
 * disclaimer in the documentation and/or other materials provided with the distribution.
 * The name of the contributor may not be used to endorse or promote products derived
 * from this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, 
 * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. 
 * IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, 
 * OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, 
 * OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 
 * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF 
 * THE POSSIBILITY OF SUCH DAMAGE. 
 *
 * Notes:
 *
 * some code to read a csv-file however there are certain caveats:
 * (1) UTF-8 format not fully supported, should work with most files but not tested with non-US fonts.
 * (2) Windows format not supported out of the box (although easily changed by changing the EOL char)
 *
 * This is just a stub which should work with most normal CSV-files however it shouldn't be too difficult
 * to modify to suit your needs and hopefully save you some time and work.
 *
 * It has only been tested on MacOSX but was originally done on Windows/Visual Studio so should work there as well.
 *
 * The intention with this module is provide something simple to do the job, not create some complicated and maybe
 * elegant way of doing it - I leave that up to you. :-)
 *
 * Usage:
 *
 * CsvParser P("/Users/ak/SampleData.csv");
 * map<int,char*> fileContent = P.parseCSV();
 * for (size_t row = 0; row < P.rows(); ++rows)
 *
 *     for (size_t col = 0; col < P.cols(); ++cols)
 *
 *          char* tok = fileContent[col + row*P.cols()]; 
 *
 * or    
 *
 * CsvParser P("/Users/ak/SampleData.csv");
 *
 * for (size_t row = 0; row < P.rows(); ++rows)
 *
 *     for (size_t col = 0; col < P.cols(); ++cols)
 *
 *			char* tok = P.token(row,col);
 */

#include <cstdio>
#include <iostream>
#include <cstdio>
#include <fstream>
#include <iostream>
#include <cassert>
#include <vector>
#include <map>
#include "CsvParser.h"

using namespace std;

/*!
    @function
    @abstract   constructor
    @discussion 
    @param      fullPath path to CSV-file
    @result     instance
*/


CsvParser::CsvParser(string fullPath) : _rows(0), _cols(0), _emptyRows(0), _fileLength(0), _fullPath(fullPath)
{
	;
}

/*!
    @function
    @abstract   destructor
    @discussion cleanup
    @param      none
    @result     none
*/


CsvParser::~CsvParser()
{
	;
}

/**
 * given a file buffer, retrieve number of rows and columns
 * @param fileBuffer pointer to the buffer containing the file
 * @param length of buffer i.e. file length
 * @param maxRows returned value after counting EOL
 * @param maxCols returned value after counting delimiters
 * @return void
 */

void CsvParser::determineRowsAndCols( const char* fileBuffer, const size_t length )
{
	long inQuotes = 0;
	size_t rows = 0; 
	size_t cols = 0;
	// determine rows and columns

	for ( const char* p = fileBuffer; p < fileBuffer + length; ++p)
	{
		switch ( *p )
		{
			case '\n':  // Linux/Unix
			case '\r':  // MacOS     windows uses both CR+LF but here we assume none-windows file.
				if ( *(p+1) != '\n' && *(p+1) != '\r' ) 
				{
					++rows;
				}
				// check for missing ending delimiter
				if ( p > fileBuffer )
				{
					if ( *(p-1) != ',' && *(p-1) != ';' )
					{
						++cols;
					}
				}
				inQuotes = 0;
				break;
			case '\"':
				++inQuotes;
				break;
			case ',':
			case ';':
				if ( inQuotes%2 == 0 )
				{
					++cols;
				}
				break;
		}
	}

	_rows = rows;
	_cols = cols/rows; 
}

/*!
    @function
    @abstract   this function reads the CSV-file into a buffer that is allocated
    @discussion this function first checks the size of the file, then allocates a buffer which it puts the file contents
    @param      none
    @result     true if something was read.
*/


bool CsvParser::readFile()
{
	ifstream fp( _fullPath.c_str(), ios::in|ios::binary );
	if ( !fp.is_open() )
	{
		return false;
	}

	// get length of file and allocate a buffer of that size
	fp.seekg(0,ios::end);
	_fileLength = fp.tellg();
	fp.seekg(0,ios::beg);
	_fileBuffer = auto_ptr<char>(new char[ _fileLength + 1 ]);
	fp.read( _fileBuffer.get(), _fileLength );
	fp.close();
	
	return _fileLength>0;
}

/*!
    @function
    @abstract   returns number of rows in the CSV-file
    @discussion 
    @param      none
    @result     rows in CSV-file
*/


size_t CsvParser::rows() const
{
	return _rows;
}

/*!
 @function
 @abstract   returns number of columns in the CSV-file
 @discussion it is assumed the CSV-file follows the standard format
 @param      none
 @result     cols in CSV-file
*/

size_t CsvParser::cols() const
{
	return _cols;
}

/*!
 @function
 @abstract   number of empty rows that have been filtered out
 @discussion normally a CSV-file shouldn't contain empty rows
 @param      none
 @result     returns the number of empty rows in the file.
*/

size_t CsvParser::emptyRows() const
{
	return _emptyRows;
}

/*!
    @class
    @abstract    location provides a simple means to keep track of the tokens
    @discussion  the location class is used internally to keep track of the position in the CSV-file
                 which is later used to retrieve the tokens.
*/

class location
{
public:
	location() : _row(0),_col(0) 
	{
		;
	}
	
	inline void newRow()
	{
		++_row;
		_col = 0;
	}
	inline void newCol()
	{
		++_col;
	}
	
	inline void resetCol()
	{
		_col = 0;
	}
	
	inline size_t col() const {
		return _col;
	}
	
	inline size_t row() const {
		return _row;
	}
protected:
	size_t _row;
	size_t _col;
};

/*!
    @function
    @abstract   parses the file and creates a representation of the CSV-file in memory
    @discussion the file is loaded into memory and then all delimiters are replaced with \0
				valid delimiters are , (comma) and ; (semicolon)
    @param      fullPath path to the CSV-file
    @result     returns a map representaion of the CSV-file, use [cols*row + col] to retrieve a token
                if the file wasn't found, an empty map is returned
*/

CsvParser::fileMap_t & CsvParser::parseCSV()
{
	// if called twice, just return previous result.
	if ( _csvFile.size() > 0 )
	{
		return _csvFile;
	}
	
	// if failed to read file or empty file, return empty map
	if ( !readFile() )
	{
		return _csvFile;
	}

	// check number of rows and columns
	determineRowsAndCols( _fileBuffer.get(), _fileLength );

	// initialize pointer to first entry in the CSV-file
	char* pPreviousToken = _fileBuffer.get();

	location loc;
	long inQuotes = 0;

	// go through the filebuffer putting pointers into the right places.
	// count the number of " found, when the ',' is found if there is an even number of " then treat , as delimiter
	// otherwise its regarded as part of the string token
	
	for ( char* p = _fileBuffer.get(); p < _fileBuffer.get() + _fileLength && loc.col() < _cols && loc.row() < _rows; ++p )
	{
		switch ( *p )
		{
			case '\"':
				++inQuotes;
				break;
			case ',':
			case ';':
				if ( inQuotes%2 == 0  ) // even or zero quotes then acknowledge delimiter
				{
					inQuotes = 0;
					*p = '\0'; 
					
					_csvFile[loc.row()*_cols+loc.col()] = pPreviousToken; 

					loc.newCol();
					pPreviousToken = p + 1; // start of next token

					// if reached EOL then skip EOL, increment row and reset col.
					if ( *pPreviousToken == '\n' || *pPreviousToken == '\r' ) 
					{
						*pPreviousToken++ = '\0'; 
						++p;
						loc.newRow();
					}
				}
				break;
			default:
				// ignore empty rows
				if ( ( *pPreviousToken == '\n' || *pPreviousToken == '\r' ) && loc.col() < _cols )
				{
					*pPreviousToken++ = '\0'; 
					loc.resetCol();
					++_emptyRows;
					inQuotes = 0;
				}
				// end of line but no ending ',' or ';'
				else if ( *p == '\n' || *p == '\r' )
				{
					*p = '\0'; 
					_csvFile[loc.row()*_cols+loc.col()] = pPreviousToken; 
					pPreviousToken = p + 1;
					inQuotes = 0;
					loc.newRow();
				}
				break;
		}
	}

	return _csvFile;
}

/*!
    @function
    @abstract   shows the content
    @discussion can be used to see how to access contents of the CSV-file
    @param      none
    @result     void
*/


void CsvParser::dump() 
{
	// print out results
	for ( size_t r = 0; r < _rows; ++r )
	{
		for ( size_t c = 0; c < _cols; ++c )
		{
			if ( c != 0 ) cout << '\t';
			if ( _csvFile[r*_cols+c] != NULL )
			{
				cout << _csvFile[r*_cols+c];
			}
			else
			{
				cout << "*";
			}
		}
		cout << endl;
	}
	cout << "empty rows: " << _emptyRows << endl;
}

/*!
    @function
    @abstract   alternative way to retrieve one value without the map
    @discussion a slower way to retrieve a token, but maybe safer
    @param      row row starting at 0 to rows()-1
    @param      col column starting at 0 to cols()-1
    @result     one string token
*/


char* CsvParser::token( size_t row, size_t col )
{
	// if parseCSV was not called previously, do it now, ignore return value
	if ( _rows == 0 && _cols == 0 )
	{
		fileMap_t ret = parseCSV();
		// if fail, return NULL
		if ( ret.size() == 0 )
		{
			return NULL;
		}
	}
	
	assert( row < _rows );
	assert( col < _cols );
	
	// avoid memory out of bonds
	if ( row >= _rows || col >= _cols )
	{
		return NULL;
	}
	
	return _csvFile[row*_cols+col];
}
